package ch.ms.persistenttimers;

import java.util.Collections;
import java.util.Date;
import java.util.Stack;
import java.util.Vector;

import org.anddev.andengine.engine.handler.IUpdateHandler;

import ch.ms.persistenttimers.actions.AbstractTimerAction;

/*
 * Copyright 2011 dunhill

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */

/**
 * A persistent timer is recreated each time, the program starts new. 
 * @author dunhill
 */
public final class PersistentTimer implements IUpdateHandler {

	private final PersistentTimerData data;
	
	protected PersistentTimer(PersistentTimerData data) {
		this.data = data;
	}
	
	public PersistentTimerData getData() {
		return data;
	}
	
	public void doStart() {
		if(!data.isStarted()) {
			onStart();
		}
	}
	
	public void addStartAction(AbstractTimerAction action) {
		data.addStartAction(action);
	}
	
	public void addTickAction(AbstractTimerAction action) {
		data.addTickAction(action);
	}

	public void addFinishAction(AbstractTimerAction action) {
		data.addFinishAction(action);	
	}
	
	
	@Override
	public void onUpdate(float pSecondsElapsed) {
		// Timer logic
		if(data.isEndless() || data.getCurrentTicks() < data.getTicks()) {
			
			// now, check if its time for one or more ticks
			
			long realTimeGapInMs = new Date().getTime() - data.getTimerStartedDate().getTime();
			
			if(data.forceTicks) {
				long toCheckGap = data.delayInMs * (data.currentTicks + 1); // we need to increase the ticks temporary..
				if(realTimeGapInMs > toCheckGap) {
					onTick();
				}
				
			} else {
				long toCheckGap = data.delayInMs;
				if(realTimeGapInMs > toCheckGap) {
					data.setTimerStartedDate(new Date());
					onTick();
				} 
			}
		} else {
			// here, we have actually the end of the timer..
			onFinish();
		}		
	}
	
	private synchronized void onStart() {
		for(AbstractTimerAction action : data.onStartActions) {
			action.call(this);
		}
	}
	
	private synchronized void onTick() {
		for(AbstractTimerAction action : data.onTickActions) {
			action.call(this);
		}
	}
	
	private synchronized void onFinish() {
		for(AbstractTimerAction action : data.onFinishActions) {
			action.call(this);
		}
	}
		
	@Override
	public void reset() {}
	
	public static class PersistentTimerData {
		
		private boolean started = false;
		
		private final boolean endless;
		
		private final long id;
		
		// if unique name is not null, the timer will not be recreated (even if forced), if there is already an instance with this name..
		private final String uniqueName;
		
		// the max ticks, before we exit..
		private final long ticks;
		
		// andengine => 10 ticks per sec.. this means we have a spare of ~29247120867.75 years before our tick 'pool' runs out!
		// watch this, its not final!
		private long currentTicks;
		
		// watch this, its not final!
		private Date timerStartedDate;
		
		/**
		 * <b>NOT IN USE</b>
		 * This lil baby is calculated from the actual delay. For example:
		 * <br> A Timer with 2 seconds delay, could use a ingame delay of about 0.1 seconds to make it more accurate.
		 * <br/> but a timer
		 *  with about 30 seconds delay only needs a ingame delay of about 0.5 seconds. This actually makes the
		 *  whole system faster.
		 */
		@SuppressWarnings("unused")
		private final float fineTunedIngameDelay;

		private final float delay;
		
		private final long delayInMs;
		
		private final boolean forceTicks;
		
		private final Stack<AbstractTimerAction> onStartActions = new Stack<AbstractTimerAction>();
		private final Stack<AbstractTimerAction> onTickActions = new Stack<AbstractTimerAction>();
		private final Stack<AbstractTimerAction> onFinishActions = new Stack<AbstractTimerAction>();
		
		
		public PersistentTimerData(long id, float delay, long ticks, long currentTicks, boolean endless,boolean forceTicks) {
			this(id,delay,ticks,currentTicks,endless,forceTicks,null);
		}
		
		public PersistentTimerData(long id, float delay, long ticks, long currentTicks, boolean endless,boolean forceTicks, String uniqueName) {
			super();
			this.id = id;
			this.delay = delay;
			this.delayInMs = (long)(delay * 1000);
			this.ticks = ticks;
			this.currentTicks = currentTicks;
			this.endless = endless;
			this.forceTicks = forceTicks;
			
			// NOTE: THIS IS NOT IN USE (YET)
			// calculate the fineTunedIngameDelay .. baby!
			if(delay <= 2) {
				// under or eq 2 second delay, we fine tune the ingame delay to zero, which means
				// every call to on update will handle this timer => most accurate!
				fineTunedIngameDelay = 0.0f;
			} else if(delay <= 10) {
				// over 2 seconds and under 10 seconds, we fine tune to 0.25 seconds, which is fast enough for them
				fineTunedIngameDelay = 0.25f;
			} else {
				// delays over 10 seconds will have a fine tuned delay of 0.5 seconds. This seems fairly accurate enough for them
				fineTunedIngameDelay = 0.5f;
			}
			this.uniqueName = uniqueName;
		}
		
		
		public void tick() {
			currentTicks++;
		}
		
		public long getId() {
			return id;
		}
		public float getDelay() {
			return delay;
		}
		public long getTicks() {
			return ticks;
		}
		public boolean isEndless() {
			return endless;
		}
		
		public boolean isForceTicks() {
			return forceTicks;
		}
		
		public void addStartAction(AbstractTimerAction action) {
			onStartActions.add(action);
		}
		
		public void addTickAction(AbstractTimerAction action) {
			onTickActions.add(action);
		}

		public void addFinishAction(AbstractTimerAction action) {
			onFinishActions.add(action);	
		}
		
		
		public Vector<AbstractTimerAction> getOnStartActions() {
			
			@SuppressWarnings("unchecked")
			Vector<AbstractTimerAction> toReturnList = (Vector<AbstractTimerAction>)onStartActions.clone();
			Collections.reverse(toReturnList);
			return toReturnList;
		}
		public Vector<AbstractTimerAction> getOnTickActions() {
			@SuppressWarnings("unchecked")
			Vector<AbstractTimerAction> toReturnList = (Vector<AbstractTimerAction>)onTickActions.clone();
			Collections.reverse(toReturnList);
			return toReturnList;
		}
		public Vector<AbstractTimerAction> getOnFinishActions() {
			@SuppressWarnings("unchecked")
			Vector<AbstractTimerAction> toReturnList = (Vector<AbstractTimerAction>)onFinishActions.clone();
			Collections.reverse(toReturnList);
			return toReturnList;
		}
		
		public long getCurrentTicks() {
			return currentTicks;
		}

		public Date getTimerStartedDate() {
			return timerStartedDate;
		}

		public void setTimerStartedDate(Date timerStartedDate) {
			this.timerStartedDate = timerStartedDate;
		}

		public boolean isStarted() {
			return started;
		}

		public void setStarted(boolean started) {
			this.started = started;
		}

		public String getUniqueName() {
			return uniqueName;
		}
	}
}